package com.siasun.vaemrd.service;


import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.siasun.vaemrd.entity.TokenDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RSetCache;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;


@Slf4j
@Service
public class AztService {

    private final RedissonClient redissonClient;

    @Autowired
    public AztService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }


    @Value("${azt.http.secretKey:IqcServerRongDa888}")
    private String httpSecretKey;
    @Value("${azt.http.appId:SmartQuality}")
    private String httpAppId;
    @Value("${azt.http.cachePath}")
    private String cachePath;
    @Value("${azt.http.time:60}")
    private int time;
    @Value("${azt.http.tokenTimeout:7200}")
    private int tokenTimeout;

    @Value("${azt.http.tokenRedisKey:iqc:custom:workBadge:tokenCache}")
    private String tokenRedisKey;

    public JSONObject uploadRec(MultipartFile file, String appId, String transactionId, String accessToken,
                                String recPath, String xtime, String signature, String mac, String audioPath) {
        /**
         * Redisson 是一个基于 Redis 的 Java 驻内存数据网格，它在 Redis 原生数据结构的基础上进行了封装和扩展。
         * RSetCache 就是 Redisson 封装后的一种数据结构,RSetCache 里的每个元素都能够设置独立的过期时间。
         * 而在 Redis 原生的集合数据结构中，并没有直接支持为单个元素设置过期时间的功能。
         *
         * RSetCache 提供了一系列 Java 方法，例如 add、remove、contains 等，方便 Java 开发者使用。
         * 这些方法隐藏了 Redis 命令的细节，让开发者可以更便捷地操作数据。
         *
         * Redisson 借助 Redis 的原生数据结构和 Lua 脚本达成了 RSetCache 的功能。
         * 它也许会结合 Redis 的集合和有序集合来存储元素和元素的过期时间，同时利用 Lua 脚本来保证操作的原子性。
         *
         */
        RSetCache<String> tokenCache = redissonClient.getSetCache(tokenRedisKey);
        JSONObject response = new JSONObject();
        if (StringUtils.isAnyBlank(appId, xtime, transactionId, mac, accessToken, recPath, signature)) {
            log.warn("Agent: upload param has empty, appId {}, xtime {}, transactionId {}, mac {}, accessToken {}, recPath {}, signature {}",
                    appId, xtime, transactionId, mac, accessToken, recPath, signature);
            return response.fluentPut("returnMessage", "parameter cannot be empty").fluentPut("returnCode", "1001");

        }
        String mySign = hmac256(httpSecretKey, appId + transactionId + accessToken + recPath + xtime + mac);
        if (StringUtils.isEmpty(mySign)) {
            log.warn("Agent: token invalid {} {} :mySign is empty.", recPath, mac);
            return response.fluentPut("returnMessage", "system error，authentication encryption failed").fluentPut("returnCode", "2999");
        }
        if (!mySign.equals(signature)) {
            log.warn("Agent: token invalid {}  {} :mySign[{}] not eq signature[{}]", recPath, mac,mySign,signature);
            return response.fluentPut("returnMessage", "authentication encryption failed").fluentPut("returnCode", "1002");
        }
        if (!tokenCache.contains(accessToken)) {
            log.warn("Agent: token timeout {}  {} :tokenCache not contains accessToken[{}].", recPath, mac,accessToken);
            return response.fluentPut("returnMessage", "token lose efficacy").fluentPut("returnCode", "1003");
        }
        tokenCache.add(accessToken, tokenTimeout, TimeUnit.SECONDS);
        //Note：audioPath若为空，默认8k音频标识路径
        if (StringUtils.isEmpty(audioPath)) {
            audioPath = "audio_8k";
        }
        File recFile = Paths.get(cachePath, audioPath, recPath).toFile();
        File fileTemp = Paths.get(cachePath, audioPath, recPath.substring(0, recPath.lastIndexOf(".")) + ".temp").toFile();
        log.info("Agent [{}] uploading record [{}] file to [{}]",mac, recPath,recFile.getAbsolutePath());
        FileUtil.mkParentDirs(recFile);
        for (int i = 0; i < 3; i++) {//重试最多3次
            //读文件流
            try {
                FileUtil.writeFromStream(file.getInputStream(), fileTemp, true);//直接写文件
                if (fileTemp.exists() && fileTemp.length() == file.getSize()) {
                    File upFlagFile = FileUtil.rename(fileTemp, recFile.getAbsolutePath(), true);
                    if (upFlagFile.exists() && upFlagFile.length() == fileTemp.length()) {
                        log.info("AgentUpload: success upload file {}, size {}, index {}", upFlagFile.getAbsoluteFile(), FileUtil.size(upFlagFile), i);
                        break;//传输完毕，for循环结束
                    }
                    log.warn("AgentUpload: file [{}] rename error, try again : {}/{}",fileTemp.getAbsolutePath(), i + 1, 3);//重试
                }
                log.info("AgentUpload: failed upload  file {}, size {}", fileTemp.getAbsoluteFile(), FileUtil.size(fileTemp));
                FileUtil.del(fileTemp);
            } catch (IOException e) {
                log.error(MessageFormat.format("Agent: rec file write fail {0} {1} ", recPath, mac), e);
                return response.fluentPut("returnMessage", "write failed").fluentPut("returnCode", "2999");
            }
        }
        //Note:3次失败也会返回0000
        return response.fluentPut("returnMessage", "success").fluentPut("returnCode", "0000");
    }

    private static String hmac256(String key, String msg) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), mac.getAlgorithm());
            mac.init(secretKeySpec);
            return Base64.encodeBase64String(mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)));

        } catch (Exception e) {
            log.error("WorkBadge", e);
        }
        return null;
    }

    public JSONObject getToken(TokenDto dto)  {
        RSetCache<String> tokenCache = redissonClient.getSetCache(tokenRedisKey);
        JSONObject response = new JSONObject();
        String appId = dto.getAppId();
        String xtime = dto.getXtime();
        String secret = dto.getSecret();
        String mac = dto.getMac();

        if (StrUtil.hasBlank(appId, xtime, secret, mac)) {
            response.put("returnMessage", "parameter cannot be empty");
            response.put("returnCode", "1001");
            log.warn("token param empty {} {} {} {}", appId, xtime, secret, mac);
            return response;
        }
        if (!httpAppId.equalsIgnoreCase(appId)) {
            response.put("returnMessage", "appId error");
            response.put("returnCode", "1001");
            log.warn("wrong appId {}", appId);
            return response;
        }

        if (!httpSecretKey.equalsIgnoreCase(secret)) {
            response.put("returnMessage", "secret error");
            response.put("returnCode", "1001");
            log.warn("wrong secret key {}", secret);
            return response;
        }
        LocalDateTime xdateTime = LocalDateTimeUtil.parse(xtime, "yyyyMMddHHmmss");
        if (LocalDateTimeUtil.between(xdateTime, LocalDateTime.now()).getSeconds() > time) {
            response.put("returnMessage", "The timestamp has expired," + time + "s effect");
            response.put("returnCode", "1001");
            log.warn("xtime timeout {}", xtime);
            return response;
        }
        String token = hmac256(secret, appId + xtime + mac);
        tokenCache.add(token, tokenTimeout, TimeUnit.SECONDS);
        response.fluentPut("returnMessage", "success").fluentPut("returnCode", "0000").fluentPut("bean", new JSONObject().fluentPut("accessToken", token).fluentPut("expiresIn", String.valueOf(tokenTimeout)));
        log.info("OK:generate token success {}", response.toJSONString());
        return response;
    }

}